JWT + Redis 双重会话管理 学习笔记
前言
本笔记基于对微服务架构中会话管理方案的学习与思考,重点探讨了 JWT + Redis 双重验证机制 的设计原理、架构权衡以及鉴权扩展方案。
一、核心设计:JWT + Redis 双重会话管理
1.1 设计目标
这个设计旨在解决 纯 JWT 和 纯 Session 各自的痛点:
| 方案 | 优点 | 缺点 |
|---|---|---|
| 纯 JWT | 无状态、易于横向扩展 | 无法主动注销、Token 较长 |
| 纯 Session | 可随时注销、可存储丰富信息 | 需要维护服务端状态、分布式困难 |
| JWT + Redis | 兼顾两者优点 | 复杂度稍高 |
1.2 代码实现
// ==================== 登录时:同时生成 JWT 和存储 Redis ====================
// 1. 存入 Redis(存储用户完整信息)
redisCache.set(
RedisKeyBuild.createRedisKey(RedisKeyManage.USER_LOGIN, code, user.getId()),
user,
tokenExpireTime,
TimeUnit.MINUTES
);
// 2. 生成 JWT(只包含 userId 等基本信息)
userLoginVo.setToken(createToken(user.getId(), getChannelDataByCode(code).getTokenSecret()));
// ==================== 网关验证时:两者都要检查 ====================
public UserVo getUser(String token, String code, String tokenSecret) {
// 第一重:解析 JWT 获取 userId(验证签名完整性)
String userId = parseToken(token, tokenSecret);
// 第二重:检查 Redis 是否存在登录态
if (StringUtil.isNotEmpty(userId)) {
userVo = redisCache.get(
RedisKeyBuild.createRedisKey(RedisKeyManage.USER_LOGIN, code, userId),
UserVo.class
);
}
// 两重验证都通过才放行
return Optional.ofNullable(userVo)
.orElseThrow(() -> new DaMaiFrameException(BaseCode.LOGIN_USER_NOT_EXIST));
}
二、核心问题探讨
2.1 我的疑问
我的理解是:用 Token 而不是 Session 其实就是为了无状态*,无需在服务端维护连接信息,节省空间。但缺点是只能解析出 id,一些信息得通过 id 查表才能拿到,这些可能会损耗一定资源。而 Session 可以存储一些信息在登录态里面,登录成功后无需查数据库。*
但是这里把用户信息存在 Redis 里,岂不是又花了维护连接的空间,还花了查询的资源和时间,仅仅只是为了能踢人?那为什么不直接用 Session + Redis 呢?
2.2 架构权衡分析
这个质疑确实直击架构本质。在微服务高并发的架构背景下,选择 JWT + Redis 而不是 Session + Redis,基于以下 4 个核心维度的考量:
维度一:微服务架构下的"透传"优势(核心差异)
Session + Redis 方案的问题:
用户请求 → 网关(查Redis) → 订单服务(再查Redis) → 票务服务(再查Redis)
↓ ↓ ↓
Redis压力 ×1 Redis压力 ×2 Redis压力 ×3
JWT + Redis 方案(本方案):
用户请求 → 网关(查Redis验证,解析JWT) → 订单服务(直接用JWT信息) → 票务服务(直接用JWT信息)
↓ ↓ ↓
Redis压力 ×1 无需查Redis 无需查Redis
💡 核心收益:网关负责"脏活累活",下游服务通过 CPU 计算验证 JWT 签名即可,极大减少了 Redis 的 QPS 压力,实现了"一次认证,处处可用"。
维度二:多端兼容性
| 方案 | Web 浏览器 | iOS/Android App | 小程序 | 第三方接口 |
|---|---|---|---|---|
| Session/Cookie | ✅ 原生支持 | ⚠️ 需要 CookieJar | ⚠️ 跨域问题 | ❌ 不友好 |
| JWT (Header) | ✅ 支持 | ✅ 标准 HTTP | ✅ 标准 HTTP | ✅ 标准 HTTP |
💡 如果项目业务 App 端流量很大,Token 形式远优于 Cookie/Session 形式。
维度三:"柔性鉴权"与降级能力
代码中有 skipCheckToken 和 checkNeedUserId 逻辑:
boolean skipCheckTokenResult = skipCheckToken(url);
// ...
if (!skipCheckTokenResult) {
// 强制查 Redis
UserVo userVo = tokenService.getUser(...);
}
高并发降级场景(如 Redis 响应变慢):
| 方案 | 降级能力 |
|---|---|
| Session 方案 | ❌ 系统直接不可用 |
| JWT + Redis | ✅ 非核心业务可只校验 JWT 签名,跳过 Redis 检查 |
💡 虽然降级时无法"踢人",但至少保证了业务不崩,用户能看能刷。
维度四:解决纯 JWT 的"无法撤销"问题
对于涉及资金交易(买票)的系统,这是安全刚需:
| 场景 | 纯 JWT | JWT + Redis |
|---|---|---|
| 用户手机丢失 | ❌ 无法强制下线 | ✅ 删除 Redis Key 即可 |
| 封禁黄牛账号 | ❌ Token 未过期仍有效 | ✅ 删除 Redis Key 即可 |
| 用户主动退出 | ❌ Token 仍然有效 | ✅ 删除 Redis Key 即可 |
💡 Redis 的存储成本,是为"安全可控"支付的保险费。
2.3 方案对比总结
| 维度 | 纯 Session + Redis | 纯 JWT | JWT + Redis (本方案) |
|---|---|---|---|
| 存储压力 | 高 (Redis) | 无 | 中/高 (Redis) |
| 带宽压力 | 低 (SessionID 短) | 高 (Token 长) | 高 (Token 长) |
| 多端兼容 | 差 (依赖 Cookie) | 好 (Header) | 好 (Header) |
| 微服务传递 | 难 (需共享 Redis) | 易 (自带信息) | 易 (网关查完后下游自带信息) |
| 踢人/注销 | ✅ 支持 | ❌ 不支持 | ✅ 支持 |
| 降级能力 | ❌ 无 (必须查库) | ✅ 强 (只验签) | ✅ 强 (可灵活配置) |
2.4 结论
📌 如果是单体 Web 应用,Session + Redis 确实是更简单的选择。
📌 但因为这是微服务 + 多端 App + 高并发抢票场景,"微服务间的信息透传" 和 "多端兼容" 的权重压倒了 "节省 Redis 空间和 Token 流量" 的成本。
📌 这是一种用 "Redis 空间"和"Token 流量" 换取 "架构灵活性"和"服务解耦" 的权衡设计。
三、鉴权逻辑分析
3.1 我的疑问
这套系统里是不是没有严格的鉴权逻辑?之前的单体项目是使用 RBAC 模型,借助 Redis 存放 Session 和 LoginVo(里面包含了用户的身份集合和权限集合),每次请求打到就 AOP 拦截先校验,通过注解的形式打在某个接口上实现接口级别的精细鉴权。
这里 Redis 里也存了 UserVo,貌似可以扩展到那种形式?
3.2 To C 与 To B 的鉴权差异
To B(管理后台)—— 功能权限
场景:内部系统、ERP、CMS
特点:用户分三六九等(超级管理员、财务、运营、普通员工)
核心:功能权限(Functional Authority)
实现:@PreAuthorize("hasRole('ADMIN')"),基于 RBAC 模型
To C(消费者端)—— 数据权限
场景:数百万普通用户来抢票
特点:99.9% 的用户角色都是"普通会员"
核心:数据权限(Data Authority)
实现:代码里的逻辑校验(currentUserId == order.userId)
💡 这套系统目前展示的是 C 端逻辑,所以没有看到复杂的 RBAC。但在后台管理模块中,RBAC 方案是标准答案。
3.3 如何扩展 RBAC 鉴权
Step 1:扩充 UserVo
public class UserVo {
private Long id;
private String username;
// 扩展:角色集合
private Set<String> roles;
// 扩展:权限标识集合 (e.g., "program:add", "order:export")
private Set<String> permissions;
}
Step 2:登录时注入权限
// 管理员登录成功后
UserVo userVo = new UserVo();
userVo.setId(user.getId());
userVo.setUsername(user.getUsername());
// 查询角色和权限
userVo.setRoles(roleService.getRolesByUserId(user.getId()));
userVo.setPermissions(permissionService.getPermissionsByUserId(user.getId()));
// 存入 Redis
redisCache.set(key, userVo, tokenExpireTime, TimeUnit.MINUTES);
Step 3:鉴权拦截位置选择
| 位置 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| 网关层鉴权 | 粗粒度拦截 | 无权限请求进不去微服务,节省资源 | 需维护 URL-权限映射,配置繁琐 |
| 服务内 AOP | 细粒度拦截 | 开发体验好,权限跟着接口走 | 请求已进入服务内部 |
推荐组合:
网关:全局黑白名单 + 登录态检查
服务内 AOP:业务强相关的细粒度拦截(如 @RequiresPermissions("user:delete"))
3.4 微服务下的特殊挑战
挑战一:内存膨胀问题
| 场景 | 用户量 | Redis 存储策略 |
|---|---|---|
| 单体 To B | 几千内部员工 | 完整权限列表,无压力 |
| 微服务 To C | 1000万活跃用户 | ⚠️ 每人存权限列表会爆内存 |
优化方案:
- C 端用户:Redis 只存基本信息,不存权限
- B 端/VIP 用户:才存储权限列表
- 使用 BitMap 或 角色 ID 代替具体的权限字符串列表
挑战二:权限变更的实时性
问题场景:
1. 在数据库里删除了管理员的权限
2. 但他的 UserVo 还在 Redis 里(TTL 30分钟)
3. 结果:他依然拥有权限,直到 Redis 过期
解决方案:
// 修改权限时,主动删除/更新该用户在 Redis 中的 UserVo 缓存
public void updateUserPermissions(Long userId, Set<String> newPermissions) {
// 1. 更新数据库
permissionMapper.updateByUserId(userId, newPermissions);
// 2. 删除 Redis 缓存(强制下次请求重新加载)
redisCache.delete(RedisKeyBuild.createRedisKey(RedisKeyManage.USER_LOGIN, code, userId));
}
四、核心理解总结
4.1 会话管理的本质
┌─────────────────────────────────────────────────────────────┐
│ │
│ Token(JWT)= 钥匙(Key) │
│ - 只是一个凭证,用于证明"我是谁" │
│ - 存储在客户端 │
│ │
│ Redis = 锁芯(Session Store) │
│ - 真正的状态存储,包含用户详细信息 │
│ - 存储在服务端,可随时控制(踢人、封禁) │
│ │
└─────────────────────────────────────────────────────────────┘
4.2 双重验证流程
客户端请求
│
▼
┌───────────────┐
│ 携带 JWT Token │
└───────┬───────┘
│
▼
┌───────────────────────┐
│ 第一重:验证 JWT 签名 │ ← CPU 计算,验证 Token 未被篡改
│ (解析出 userId) │
└───────────┬───────────┘
│
▼
┌───────────────────────┐
│ 第二重:查询 Redis │ ← IO 操作,验证登录态是否存在
│ (检查是否被踢/过期) │
└───────────┬───────────┘
│
┌───────┴───────┐
│ │
▼ ▼
✅ 放行 ❌ 拒绝
4.3 鉴权层次
| 层次 | 关注点 | 实现方式 |
|---|---|---|
| 认证(Authentication) | 你是谁? | JWT 解析 + Redis 状态检查 |
| 授权(Authorization) | 你能做什么? | RBAC + AOP 注解 |
| 数据权限(Data Scope) | 你能看哪些数据? | 业务代码逻辑校验 |
五、设计启示
5.1 架构选型不是非黑即白
没有"最好"的方案,只有"最适合"的方案。JWT + Redis 看似"两者缺点都占了",但在特定场景下却是最优解。
5.2 理解场景是关键
单体应用 + Web 端 → Session + Redis 就够了
微服务 + 多端 + 高并发 → JWT + Redis 更合适
5.3 安全与性能的权衡
Redis 存储成本 = 安全可控的保险费
在涉及资金交易的系统中,"能踢人"不是可选项,而是刚需。
企业级项目导航:⬅️ 04-(缓存预热)购票人业务架构分析 | 05-JWT + Redis 双重会话管理 学习笔记 | ➡️ 06-Lua 深度学习笔记
💬 评论